内容
本篇文章着眼是如何编写优雅的 React 代码.我们会结合 React和Ramda
,以函数式风格来编写应用.所有的概念对于 lodash/fp其他的函数式编程库也是适合的.关键在于你使用哪种库.
本文示例使用*eslint-config-cleanjs*
,来强化函数式风格,其中包括no-this和no-classes规则.这些函数式规则可以让我在开始下面的示例时采用更规范.如果你对配置感兴趣,可以到 github 仓库看看具体的规则设置
Compose组件
让我们从更为容易接受的方法开始吧!看看下面的代码:
1 | const comp=(f,g)=>x=>f(g(x)) |
如果是组件,实现代码是这样的:
1 | const TodoList=(List,mapItems)=>s=>List(mapItems(s)) |
这么做就有意义了,可以让我们通过compose
小的组件来构建更大的组件.
1 | const List = c => <ul>{c}</ul> |
TodoList是一个函数等待应用的 state并且根据对应的 state 返回新的组件.代码非常简洁,有意义.我们遍历了一些 todo 的条目,并由此创建Item列表.之后结果借助 props 传递给 TodoList,并且在组件内部渲染.
牢记:
1 | const App = state => List(map(Item, state))) |
组件渲染其他子组件的想法工作良好,编写的大多数组件都不依赖JSX,只依赖传递进入的 state.所以这个方法只对小的子组件起作用.
下面是使用 Ramda map,compose,prop 方法的实例代码:
1 | import React from 'react' |
Compose和组件的限制
现在我们已经可以compose 组件并渲染出 Todo list. 接下来看看更为常见的方法,也就是 props 可以自顶向下传递.这些 props可以是任何内容,包括回调函数,其他组件以及数组和对象.下面的代码和 Todo List 的代码相同,只是包含额外的 Header 组件
1 | const Header = title => <h1>A Todo List: {title}</h1> |
没有什么特别的,但是看看回头看看之前的实现方法,明确的表明,在 List 中包含一个 header 是不可能的.之前的compse:
1 | const TodoList = compose(List, map(Item), getTodos) |
实际编程中我们需要能够 compose Header 和 List 的方法, Header放在哪里合适? 我们向 TodoList 函数传递了应用的state,接着经过筛选,然后遍历筛选过的 todos创建Items 的数组,然后传递给 List.Header组件如何才能从 state 中获取到标题信息?需要更好的办法.
说的更明白一点:
1 | const TodoHeader = mapStateToProps => Header(mapStateProps) |
订正 这里哟一个更好的实现方法(感谢Thai Pangsakulyanont)
1 | const TodoHeader = todoState => |
我们希望是传递应用的 state接着使所有的组件各取所需的 properties.为了让思路更清晰,调用 mapStateToProps
函数
1 | const mapStateToProps = curry((f, g) => compose(g, f)) |
mapStateToProps
等待一个函数还有组件,之后首先针对提供的 state 应用函数,之后结果传递给组件.需要注意这一点,我们柯理化了函数,仅仅是想把筛选 state的定义和实际的组件分离开. 在这个问题上, Ramda的绝大多数函数都是自动柯理化的.
下面代码是如何在 Header组价中应用mapStateToProps
.
1 | const TodoHeader = mapStateToProps(s => s.title, Header) |
这看起来和 react-redux 的connect()函数和类似了.我们使用mapStateToProps
把state的特定部分转化为 props. 现在 Header 和之前的 List组价可以分别获取各自的 state信息了.
1 | const TodoList = mapStateToProps(getTodos, compose(List, map(Item)) |
显然,mapStateToProps
只解决了一部分问题,我们仍然需要 compose TodoList 和 Header来创建整个应用的能力.
使用 compose 不能解决这个问题.所以来实现我们自己的工具函数combine
.需要两个组件并返回一个新的组件.
1 | const combine = curry((c, o) => x => (<div>{c(x)} {o(x)}</div>)) |
使用 combine
函数可以 compose Header 和 List创建新的函数.
1 | const TodoHeader = mapStateToProps(s => s.title, Header) |
现在 compose 两个分别获取特定state 的方式已经有了.接着更进一步看看怎么 compose 更多的组件
Reducing组件
如果需要在应用中添加一个显示当前年份的 Footer 组件.怎么才能做到这一点? 首先想到的办法是:
1 | const App = combine(TodoHeader, combine(TodoList, TodoFooter)) |
首先Combine TodoList 和 TodoFooter,接着再 combine 之前的结果和 TodoHeader.这么做是可行的,但是如果组件再多一点,代码就不太好懂了.
可以考虑像下面一样操作:
1 | // array of components |
有了想法,看看实际的实现
1 | const combineComponents = (...args) => { |
参考 redux中的combineReducers
,我们把自己的 reducer 称为combineComponents
, combineComponents
接收一组组件,并 Reduce为等待组件 state 的单个函数.
1 | const App = combineComponents(TodoHeader, TodoList, TodoFooter) |
有了mapStateToProps
, combine
, combineComponents
的协助,我们现在就可以 compose 组件了.考虑到 mapStateToProps, 我们可以最一下最后的提炼.看看刚开始的实现方法
1 | const mapStateToProps = curry((f, g) => compose(g, f)) |
实际上,我们完全没有必要自己实现它.Ramda或者 lodash/fp 已经提供了一个函数:pipe
. pipe
函数从左至右运行所有的函数.看看下面的例子
1 | const add = x => x + 1 |
所以pipe
和 compose
很像,只不过参数方向是相反的. 我们使用了 Ramda的flip
函数,这个函数在本实例中 翻转两个参数的方向.意味着,我们现在可以重构 mapStatToProps
为:
1 | const mapStateToProps = pipe |
或者直接使用pipe
函数,让 Ramda 担起全责.这么做以后,留给我们两个函数combine
和combineRedcers
需要处理.甚至combine 函数都可以隐藏起来,但是为了推理清晰一点,还是保留吧!
完整的代码如下:
1 | import React from 'react' |
Adding Redux
Reduce一切组件? 下面的伪代码,帮助我们创建要达成目标的心里模型
1 | const App=(state,action)=>TodoList |
上面的代码看起来有点像典型的 Redux reducers,不同点是我们在这里返回的是一个 React 组件,并不是经过计算的state.如果要借助 Readux 来完成这个目标? 试试看
我们仍然来构建一个 TodoList,并且保持清晰明了,会使用译注 redux todomvc 的actions 和 todo reducer.
1 |
|
部分原始的reducer 代码通过使用 Ramda的 reject
和propEq
重构来过滤已经删除掉的 todo条目.万一你想知道reject
是什么函数,reject
是 filter的补集.我们可以编写一组助手函数:
1 | // redux utils |
getNextId
是用于获取下一个 id 的函数,在添加新的条目是,需要用到它.createReducer
已经在在 Redux的顶层输出中出现了,但是这里的是使用 Ramda重写的版本.
现在我们已经有了 reducers和 Action.现在需要适配他们和我们的组件以便于处理添加和删除组件.为了保持简单,我们用一个 add 按钮来代替输入文本添加 todo项文本的操作.
1 | const Add = onSave => ( |
最后还需要一个删除按钮.在 Item组件中添加一个删除按钮就足够了.
1 | const Item = ({todo, removeTodo }) => ( |
需要的部分都已经就绪了.这里仍然有一些部分需要澄清:removeTodo
应该 dispatch deleteTodo
action. 另一个需要考虑的方面是,我们需要一个方法定义必须要提供的 dispatcher. 现在我们还仅仅是映射 state 到 props.
来添加一个 getRender
函数,等待输入应用入口节点,返回一个等待 React 组件的函数.
1 | const getRender = node => app => ReactDOM.render(app, node) |
接下来编写一个 bindActionCreator.
1 | // define a bindActionCreator |
接着隐藏掉 dispatch
方法,同时传递bindActionCreator
和 state 到应用,并且订阅到 Redux 的 store,当代触发渲染. 要声明一下, Redux 已经有可以直接使用的 bindActionCreators 函数.
1 | const run = store.subscribe(() => |
最后的一些收尾工作是适配 Item和 TodoList 组件, Items 期待 todo条目还有 onDelete
函数
1 | const Item = ({todo, onDelete}) => ( |
因为现在 Item组件也需要onDelete
函数,我们需要适配map to props
函数.我们已经获取了 dispatch,所以返回一个todo items 的数组了,需要范湖一个包含 todo数组和onDelete
函数的对象.
1 | // for clearer understanding extracted mapItems |
Outro
这篇文章的目的是介绍如何联合 Ramda,React和 Redux来编写更加优雅的代码.
例子只是用来说明如何在React 或者 Redux 中使用 Ramda.在实际的编程中,你可以在应用的某些部分借助Ramda或者 lodash/fp 来编写优雅的代码.
例如可以重构mapDispatchToProps
函数,根据定义好的 propTypes
自动映射 state到应用的props,代替手动输入.
1 | const getPropTypes = prop('propTypes') |
也可以使用 Ramda 的 pick
函数替代 mapDispatchToProps 函数.
1 | export default connect(pick(['todos']))(App) |
If you have any questions or feedback don’t hesitate to leave feedback @ twitter.
这篇文章受到Brian Lonsdorf
在 React Rally上演讲的启发.